<head>
  <style>
    #playground {
      width: 800px;
      height: 600px;
      border: 2px solid;
    }

    .player {
      width: 50px;
      height: 50px;
      position: absolute;
      background-color: gray;
      border: 1px solid green;
      text-align: center;
      line-height: 50px;
      font-size: 20px;
      cursor: move;
    }
  </style>
</head>
<div id="playground"></div>
<script>
  class WsService {
    constructor() {
      this._ws = null;
      this._lastSeq = 0;
      this._getIdPromise = null;
      this._getIdResolve = null;
      this._id = "";

      this.onmousedown = null;
      this.onmousemove = null;
      this.onmouseup = null;
      this.onaddplayer = null;
      this.allplayers = null;
    }

    openConn() {
      return new Promise((resolve, reject) => {
        this._ws = new WebSocket("ws://127.0.0.1:8080");
        this._ws.onopen = resolve;
        this._ws.onclose = reject;
        this._ws.onerror = reject;
        this._ws.onmessage = (ev) => this.onMessage(ev);
        this._getIdPromise = new Promise((resolve) => {
          this._getIdResolve = resolve;
        });
      });
    }

    sendAction(data) {
      this._send({ id: this._id, cmd: "act", data });
    }

    onMessage(ev) {
      const dataObj = JSON.parse(ev.data);
      const { seq, cmd, data } = dataObj;
      if (cmd === "start") {
        this._lastSeq = seq;
      } else if (cmd === "id") {
        this._id = data.id;
        this._getIdResolve(data);
      } else if (cmd === "add") {
        if (this._lastSeq + 1 !== seq) {
          console.error("package lost", seq);
        }
        this._lastSeq = seq;
        this.onaddplayer && this.onaddplayer(data.id, data);
      } else if (cmd === "allplayers") {
        console.log("allplayers", data);
        this.onallplayers && this.onallplayers(data);
      } else if (cmd === "act") {
        if (this._lastSeq + 1 !== seq) {
          console.error("package lost", seq);
        }
        this._lastSeq = seq;

        for (const act of data) {
          this.playAct(act);
        }
      }
    }

    playAct(action) {
      const { act, data, id } = action;
      switch (act) {
        case "mousedown":
          console.log("mousedown", id, data);
          this.onmousedown && this.onmousedown(id, data);
          break;
        case "mousemove":
          console.log("mousemove", id, data);
          this.onmousemove && this.onmousemove(id, data);
          break;
        case "mouseup":
          console.log("mouseup", id, data);
          this.onmouseup && this.onmouseup(id, data);
          break;
      }
    }

    _send(msg) {
      this._ws.send(JSON.stringify(msg));
    }

    getId() {
      return this._getIdPromise;
    }
  }

  function insertPlayer(v, x, y) {
    console.log("insertplayer", x, y);
    const p = document.createElement("div");
    p.setAttribute("class", "player");
    p.textContent = v;
    p.style.left = x;
    p.style.top = y;
    const playground = document.getElementById("playground");
    playground.appendChild(p);
    return p;
  }

  function attachOperation(div, wsService) {
    dragElement(div, wsService);
  }

  function handleDrag(playerMap, wsService) {
    function onMouseDown(id, { x, y }) {
      const p = playerMap.get(id);
      if (p) {
        p.mousedown = true;
        p.pos3 = x;
        p.pos4 = y;
      }
    }

    function onMouseMove(id, { x, y }) {
      const p = playerMap.get(id);
      if (p && p.mousedown) {
        const pos1 = p.pos3 - x;
        const pos2 = p.pos4 - y;
        const ele = p.ele;
        p.pos3 = x;
        p.pos4 = y;
        ele.style.top = ele.offsetTop - pos2 + "px";
        ele.style.left = ele.offsetLeft - pos1 + "px";
      }
    }

    function onMouseUp(id) {
      const p = playerMap.get(id);
      if (p) {
        p.mousedown = false;
        p.pos3 = null;
        p.pos4 = null;
      }
    }

    wsService.onmousedown = onMouseDown;
    wsService.onmousemove = onMouseMove;
    wsService.onmouseup = onMouseUp;
  }

  function dragElement(ele, wsService) {
    ele.onmousedown = dragMouseDown;

    function dragMouseDown(e) {
      wsService.sendAction({
        act: "mousedown",
        data: { x: e.clientX, y: e.clientY },
      });

      e.preventDefault();

      document.onmouseup = closeDragElement;
      // call a function whenever the cursor moves:
      document.onmousemove = elementDrag;
    }

    function elementDrag(e) {
      wsService.sendAction({
        act: "mousemove",
        data: { x: e.clientX, y: e.clientY },
      });
      e = e || window.event;
      e.preventDefault();
    }

    function closeDragElement() {
      wsService.sendAction({
        act: "mouseup",
        data: null,
      });
      /* stop moving when mouse button is released:*/
      document.onmouseup = null;
      document.onmousemove = null;
    }
  }

  function handleAddPlayer(playerMap, wsService) {
    wsService.onaddplayer = (id, { x, y }) => {
      addPlayer(playerMap, id, x, y);
    };
  }

  function addPlayer(playerMap, id, x, y) {
    if (!playerMap.has(id)) {
      const div = insertPlayer(id, x, y);
      playerMap.set(id, { ele: div });
    }
  }

  function handleAllPlayers(playerMap, wsService) {
    wsService.onallplayers = (allplayers) => {
      for (const p of allplayers) {
        addPlayer(playerMap, p.id, p.x, p.y);
      }
    };
  }

  async function main() {
    const wsService = new WsService();
    await wsService.openConn();
    const { id, x, y } = await wsService.getId();
    const div = insertPlayer(id, x, y);
    attachOperation(div, wsService);

    const playerMap = new Map();
    playerMap.set(id, { ele: div });

    handleAllPlayers(playerMap, wsService);
    handleAddPlayer(playerMap, wsService);
    handleDrag(playerMap, wsService);
  }

  main();
</script>
